package fr.inserm.u915.tabixserver;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;


import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import fr.inserm.u915.tabixserver.expr.ExprLangParser;



public class ModelFactory
	{
	static private final Logger LOG=Logger.getLogger("tabix");
	private Model model;
	private Map<String, Table> id2table;
	private File xmlFile;

	public ModelFactory()
		{
		}
	
	public synchronized Model parse(File xmlFile) throws IOException
		{
		this.model=null;
		this.id2table=null;
		this.xmlFile=xmlFile;
		LOG.info("parsing "+xmlFile);
		DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
		factory.setCoalescing(true);
		factory.setExpandEntityReferences(true);
		factory.setValidating(false);
		factory.setIgnoringComments(true);
		try {
			DocumentBuilder builder=factory.newDocumentBuilder();
			Document dom=builder.parse(xmlFile);
			Element root=dom.getDocumentElement();
			if(root==null) throw new IOException("not root");
			if(!root.getTagName().equals("config")) throw new IOException("not a <config> root in "+xmlFile);
			this.model=new Model();
			this.id2table=new HashMap<String, Table>();
			
			for(Node n1=root.getFirstChild();n1!=null;n1=n1.getNextSibling())
				{
				if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
				Element e1=(Element)n1;
				if(!e1.getTagName().equals("user")) continue;
				parseUser(e1);
				}
			
			for(Node n1=root.getFirstChild();n1!=null;n1=n1.getNextSibling())
				{
				if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
				Element e1=(Element)n1;
				if(!e1.getTagName().equals("group")) continue;
				parseGroup(e1);
				}
			
			
			for(Node n1=root.getFirstChild();n1!=null;n1=n1.getNextSibling())
				{
				if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
				Element e1=(Element)n1;
				if(!e1.getTagName().equals("table")) continue;
				parseTable(e1);
				}
			for(Table t:this.id2table.values())
				{
				this.model.getTables().add(t);
				}
			
			for(Node n1=root.getFirstChild();n1!=null;n1=n1.getNextSibling())
				{
				if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
				Element e1=(Element)n1;
				if(!e1.getTagName().equals("build")) continue;
				parseBuild(e1);
				}
			
			//create default build if needed
			if(this.model.getBuilds().isEmpty())
				{
				Build b=new Build();
				b.setId("default");
				b.setLabel("Default Build");
				this.model.putBuild(b);
				}
			Model m=this.model;
			this.model=null;
			this.xmlFile=null;
			this.id2table=null;
			return m;
			}
		catch (ParserConfigurationException err)
			{
			throw new IOException(err);
			}
		catch (SAXException err)
			{
			throw new IOException(err);
			}
		}

	
	private void parseBuild(Element root) throws IOException
		{
		Attr att= root.getAttributeNode("id");
		if(att==null || att.getValue().trim().isEmpty())
			{
			throw new IOException("@id missing in "+root.getTagName());
			}
		Build build=new Build();
		build.setId(att.getValue());
		if(this.model.getBuildById(build.getId())!=null)
			{
			throw new IOException("build defined twice: @id="+build.getId());
			}
		labelAndDescription(build,root);
		
		File baseDir=null;
		att= root.getAttributeNode("base-dir");
		if(att==null) root.getAttributeNode("base");
		if(att==null) root.getAttributeNode("dir");
		if(att!=null )
			{
			baseDir=new File(att.getValue());
			}
		else
			{
			baseDir=this.xmlFile.getParentFile();
			}
		build.setBaseDir(baseDir);
		if(!build.getBaseDir().exists())
			{
			throw new IOException("@base-dir doesn't exist in "+ build.getBaseDir());
			}

		
		att= root.getAttributeNode("fasta");
		if(att!=null )
			{
			build.setFastaFile(new File(build.getBaseDir(),att.getValue()));
			if(!build.getFastaFile().exists())
				{
				throw new IOException("@fasta doesn't exist in "+ build.getFastaFile());
				}
			}
		
		att= root.getAttributeNode("taxon-id");
		if(att!=null )
			{
			try
				{
				build.setTaxonId(Integer.parseInt(att.getValue()));
				}
			catch (NumberFormatException e)
				{
				throw new IOException("bad taxon-id:"+att.getValue());
				}
			}
		parseListOfInstance(build,build,root);
		this.model.putBuild(build);
		}
	
	private Section parseSection(Build build,Element root ) throws IOException
			{
			Section section=new Section();
			labelAndDescription(section, root);
			parseListOfInstance(build,section,root);
			return section;
			}

	
	private void parseListOfInstance(Build build,ListOfInstances owner,Element root) throws IOException
		{
		LOG.info(owner.getId());
		for(Node n1=root.getFirstChild();n1!=null;n1=n1.getNextSibling())
			{
			if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
			Element e1=(Element)n1;
			if(e1.getTagName().equals("instance"))
				{
				parseInstance(build,owner,e1);
				//owner.addInstance(i); don't need
				}
			else if(e1.getTagName().equals("section"))
				{
				Section section=parseSection(build,e1);
				owner.addSection(section);
				}
			}
		}
	
	
	private void parseInstance(Build build,ListOfInstances owner,Element root) throws IOException
		{
		LOG.info("ici");
		Attr att= root.getAttributeNode("id");
		if(att==null || att.getValue().trim().isEmpty())
			{
			throw new IOException("@id missing in "+root.getTagName());
			}
		Instance instance=new Instance();
		instance.setBuild(build);
		instance.setId(att.getValue());
		labelAndDescription(instance,root);
		
		if(build.getDeepInstanceById(instance.getId())!=null)
			{
			throw new IOException("instance defined twice: @id="+instance.getId());
			}
		Table table=null;
	    att= root.getAttributeNode("table");
	    if(att==null) att= root.getAttributeNode("table-ref");
		if(att==null)
			{
			table= this.id2table.get(instance.getId());
			}
		else
			{
			table= this.id2table.get(att.getValue());
			}
		if(table==null) throw new IOException("Cannot associate a table to instance "+instance.getId());
		instance.setTable(table);
		LOG.info("adding "+instance+" to "+owner);
		owner.addInstance(instance);
		
		
		
		att= root.getAttributeNode("path");
		if(att==null || att.getValue().trim().isEmpty())
			{
			throw new IOException("@path missing in "+root.getTagName());
			}

		instance.setPath(new File(build.getBaseDir(),att.getValue()));
		if(!instance.getPath().exists())
			{
			throw new IOException("@path doesn't exist in "+ instance.getPath());
			}
		
		//URL
		att= root.getAttributeNode("url-expr");
		if(att!=null && !att.getValue().isEmpty())
			{
			try
				{
				instance.setUrlExpr(ExprLangParser.compile(att.getValue()));
				}
			catch(Exception err)
				{
				throw new IOException(err);
				}
			}
		//Group
		att= root.getAttributeNode("group");
		if(att==null) att= root.getAttributeNode("group-id");
		if(att==null) att= root.getAttributeNode("group-ref");
		if(att==null) att= root.getAttributeNode("group-href");
		if(att!=null)
			{
			Group g=this.model.getGroupById(att.getValue().trim());
			if(g==null) throw new IOException("@group "+att.getValue()+" doesn't exist");
			instance.setGroup(g);
			}
		}
	
	private void parseTable(Element root) throws IOException
		{
		Attr att= root.getAttributeNode("id");
		if(att==null || att.getValue().trim().isEmpty()) throw new IOException("@id missing in "+root.getTagName());
		Table t=new Table();
		t.setId(att.getValue());
		if(this.id2table.get(t.getId())!=null)
			{
			throw new IOException("table defined twice: @id="+t.getId());
			}
		this.id2table.put(t.getId(), t);
		labelAndDescription(t,root);
		for(Node n1=root.getFirstChild();n1!=null;n1=n1.getNextSibling())
			{
			if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
			Element e1=(Element)n1;
			if(!e1.getTagName().equals("column")) continue;
			Column c=parseColumn(e1);
			if(t.getColumnByName(c.getId())!=null)
				{
				throw new IOException("column @id "+c.getId()+" defined twice in "+root.getTagName());
				}
			c.setColumnIndex(t.size());
			t.putColumn(c);
			if(t.getChromColumn()==null && c.looksLikeChrom())
				{
				t.setChromColumn(c);
				}
			if(t.getStartColumn()==null && c.looksLikeStart())
				{
				t.setStartColumn(c);
				}
			if(t.getEndColumn()==null && c.looksLikeEnd())
				{
				t.setEndColumn(c);
				}
			
			if(t.getNameColumn()==null && c.looksLikeName())
				{
				
				t.setNameColumn(c);
				}
			

			if(t.getStrandColumn()==null && c.looksLikeStrand())
				{
				t.setStrandColumn(c);
				}
			}
		
		//first base type
		att= root.getAttributeNode("delim");
		if(att!=null)
			{
			t.setPattern(att.getValue());
			}
		
		//first base type
		att= root.getAttributeNode("first-base");
		if(att!=null)
			{
			String s=att.getValue();
			if(s.equals("0") || s.equals("zero"))
				{
				t.setFirstBase(FirstBase.ZERO);
				}
			else if(s.equals("1") || s.equals("one") || s.equals("+1"))
				{
				t.setFirstBase(FirstBase.ONE);
				}
			else
				{
				throw new IOException("bad value \""+s+"  in @"+att.getName());
				}
			}
		//chromosome naming
		att= root.getAttributeNode("chrom-naming");
		if(att!=null)
			{
			try
				{
				t.setChromosomeNaming(att.getValue().trim());
				}
			catch(Exception err)
				{
				throw new IOException("Bad chrom-naming class:"+att.getValue(),err);
				}
			}
		
		
		att= root.getAttributeNode("chrom-column");
		if(att!=null)
			{
			Column c=t.getColumnByName(att.getValue());
			if(c==null) throw new IOException("Cannot find column \""+att.getValue()+"  in table "+t.getId());
			t.setChromColumn(c);
			}
		att= root.getAttributeNode("start-column");
		if(att!=null)
			{
			Column c=t.getColumnByName(att.getValue());
			if(c==null) throw new IOException("Cannot find column \""+att.getValue()+"  in table "+t.getId());
			if(c.getDataType()!=AtomicType.INT)  throw new IOException("Not an integer column: start-column \""+c.getId()+"\"  in table "+t.getId());
			t.setStartColumn(c);
			}
		att= root.getAttributeNode("end-column");
		if(att!=null)
			{
			Column c=t.getColumnByName(att.getValue());
			if(c==null) throw new IOException("Cannot find column \""+att.getValue()+"  in table "+t.getId());
			if(c.getDataType()!=AtomicType.INT)  throw new IOException("Not an integer column: end-column \""+c.getId()+"\"  in table "+t.getId());
			t.setEndColumn(c);
			}
		att= root.getAttributeNode("name-column");
		if(att!=null)
			{
			Column c=t.getColumnByName(att.getValue());
			if(c==null) throw new IOException("Cannot find column \""+att.getValue()+"  in table "+t.getId());
			t.setStartColumn(c);
			}
		att= root.getAttributeNode("strand-column");
		if(att!=null)
			{
			Column c=t.getColumnByName(att.getValue());
			if(c==null) throw new IOException("Cannot find column \""+att.getValue()+"  in table "+t.getId());
			t.setStrandColumn(c);
			}
		
		
		//URL
		att= root.getAttributeNode("url-expr");
		if(att!=null && !att.getValue().isEmpty())
			{
			try
				{
				t.setUrlExpr(ExprLangParser.compile(att.getValue()));
				}
			catch(Exception err)
				{
				throw new IOException(err);
				}
			}
		
		
		if(t.getChromColumn()==null)
			{
			throw new IOException("No chrom-column in table "+t.getId());
			}
		if(t.getStartColumn()==null)
			{
			throw new IOException("No start-column in table \""+t.getId()+"\" ("+t.getColumns()+")");
			}
		if(t.getEndColumn()==null)
			{
			throw new IOException("No end-column in table "+t.getId());
			}
		}
	
	private void parseUser(Element root) throws IOException
		{
		Attr att= root.getAttributeNode("id");
		if(att==null) att= root.getAttributeNode("login");
		if(att==null) att= root.getAttributeNode("user");
		if(att==null || att.getValue().trim().isEmpty() || att.getValue().trim().equals("anonymous"))
			{
			throw new IOException("@id missing/bad value in "+root.getTagName());
			}
		String login=att.getValue().trim();
		att= root.getAttributeNode("sha1sum");
		if(att==null) att= root.getAttributeNode("sha1");
		if(att==null || att.getValue().trim().isEmpty())
			{
			throw new IOException("@sha1sum missing in "+root.getTagName());
			}
		String sha1sum=att.getValue().trim();

		User u=new User(login, sha1sum);
		if(this.model.getUserById(u.getLogin())!=null)
			{
			throw new IOException("User.id="+u.getLogin()+" defined twice");
			}
		this.model.putUser(u);
		}
	
	private void parseGroup(Element root) throws IOException
		{
		Attr att= root.getAttributeNode("id");
		if(att==null || att.getValue().trim().isEmpty())
			{
			throw new IOException("@id missing/bad value in "+root.getTagName());
			}
		
		Group g=new Group(att.getValue().trim());
		Set<String> users=new HashSet<String>();
		for(Node n1=root.getFirstChild();n1!=null;n1=n1.getNextSibling())
			{
			if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
			Element e1=(Element)n1;
			if(!e1.getTagName().equals("user")) continue;
		    att= e1.getAttributeNode("id");
		    if(att==null) att= root.getAttributeNode("href");
		    if(att==null) att= root.getAttributeNode("ref");
		    if(att==null) att= root.getAttributeNode("login");
		    if(att==null || att.getValue().trim().isEmpty())
				{
				throw new IOException("user-id missing under "+root.getTagName());
				}
		    String login=att.getValue().trim();
		    User u=this.model.getUserById(login);
		    if(u==null)
		    	{
				throw new IOException("unknown user id="+login+" under "+root.getTagName());
		    	}
		    users.add(u.getLogin()); 
			}
		g.setUserIds(users);
		this.model.putGroup(g);
		}
	
	
	private Column parseColumn(Element root) throws IOException
		{
		Attr att= root.getAttributeNode("id");
		if(att==null || att.getValue().trim().isEmpty()) throw new IOException("@id missing in "+root.getTagName());
		Column c=new Column();
		c.setId(att.getValue());
		att= root.getAttributeNode("ignore");
		if(att!=null && Boolean.parseBoolean(att.getValue()))
			{
			c.setIgnore(true);
			}

		
		labelAndDescription(c,root);
		att= root.getAttributeNode("ignore");
		if(att!=null && Boolean.parseBoolean(att.getValue()))
			{
			c.setIgnore(true);
			}
		att= root.getAttributeNode("type");
		if(att==null) att=root.getAttributeNode("dataType");
		if(att!=null)
			{
			c.setDataType(AtomicType.STRING);
			for(AtomicType t:AtomicType.values())
				{
				if(		att.getValue().equalsIgnoreCase(t.getXsdType()) ||
						att.getValue().equalsIgnoreCase(t.name()))
					{
					c.setDataType(t);
					}
				}
			
			}
		//URL
		att= root.getAttributeNode("url-expr");
		if(att!=null && !att.getValue().isEmpty())
			{
			try
				{
				c.setUrlExpr(ExprLangParser.compile(att.getValue()));
				}
			catch(Exception err)
				{
				throw new IOException(err);
				}
			}
		
		return c;
		}
	
	private void labelAndDescription(Named named,Element root)
		{
		String label=null;
		String description=null;
		for(Node n1=root.getFirstChild();n1!=null;n1=n1.getNextSibling())
			{
			if(n1.getNodeType()!=Node.ELEMENT_NODE) continue;
			Element e1=(Element)n1;
			if(e1.getTagName().equals("label"))
				{
				label=e1.getTextContent();
				}
			else if(e1.getTagName().equals("description"))
				{
				description=e1.getTextContent();
				}
			}
		if(label==null)
			{
			Attr att= root.getAttributeNode("label");
			if(att!=null &&  !att.getValue().trim().isEmpty())
				{
				label=att.getValue().trim();
				}
			}
		if(description==null)
			{
			Attr att= root.getAttributeNode("description");
			if(att!=null &&  !att.getValue().trim().isEmpty())
				{
				description=att.getValue().trim();
				}
			}
		
		if(label!=null) named.setLabel(label);
		if(description!=null) named.setDescription(description);
		}
	
	public static void main(String[] args)
		{
		ModelFactory app=new ModelFactory();
		for(String arg:args)
			{
			System.out.print(arg);
			try {
				app.parse(new File(arg));
				System.out.println("\tOK");
				}
			catch (Throwable e) {
				e.printStackTrace();
				System.out.println("\tERROR:"+e.getMessage());
				}
			}
		}
	
	}
